Skip to content

Conversation

@jdoucett
Copy link
Contributor

Phase-2 slice 5 — full restore (the capstone, RFC §2.9)

New restore.py: reassembles a size-routed target from its
{log_dir}/_vault/{badge}.manifest.json as a disjoint union of objects
extract every current object, verify, reconcile. No overlay ordering, no
deletion replay (that superset stays in deferred Phase-3 §3.4).

This is what makes aggregate reclaim --delete trustworthy: the object set is
provably round-trippable off tape. Operator-invoked — NOT wired into
reclaim --delete (a full restore pulls every tar, TB-scale).

Gates (each aborts the whole restore — never partial)

  1. Completeness gate BEFORE any transfer — every selected object's
    fortress_tar must exist (reclaim.hsi_exists) or ABORT
    "not fully restorable"; never "restored minus one".
  2. Empty-dest guard — restore only into a new/empty directory, never over
    live data.
  3. Per-object extract by recorded transport
    htar -xvf <tar> -L <memberlist> (random-access indexed; reads only the
    requested members) / hsi get + tar xf for htar_large
    (invariant revert(skip-unchanged): drop in-engine incremental Phase 1a #6: never htar_large -xvf through a pipe).
  4. Per-file MD5 verify against each object's own run-log source_files
    (arcname-keyed — invariant Canonical-root invariant: paths reclaim can never delete #2). Missing run-log or mismatch = ABORT.
  5. Mandatory final reconciliation on a full restore:
    set(walk(dest)) == ⋃ objects' arcnames, else ABORT.

Selective restore

--slot solo:<hex> / --slot shard:<k> / --member <relpath> filter the
object set (single-file restore falls out as a flag). Gate 1 checks only the
selected tars; reconciliation runs only on a full restore.

CLI

python3 restore.py config.json --dest /scratch/.../restore_here
python3 restore.py --manifest /depot/.../logs/_vault/<badge>.manifest.json --dest DIR
python3 restore.py config.json --dest DIR --slot shard:3
python3 restore.py config.json --dest DIR --member sub/path/file.bin

LOCAL only (hsi/htar at /opt/hsi/bin, prepended like ship_object). New file
off main — not stacked on #24; no engine files touched.

Tests

tests/test_restore.py (17) — tape mocked (reclaim.hsi_exists + injected
extract fn, mirroring ship_one): full round-trip reconstructs the exact
aggregate arcname set (names AND bytes); partial-GONE aborts before any
extraction with dest untouched; --slot/--member restore exactly one
object/file; GONE tar outside the selection doesn't block a selective restore;
MD5 mismatch aborts; missing run-log aborts; non-empty dest / dest-is-a-file
refused; stray file fails reconciliation.

Full suite: 238 → 255 green (python3 -m unittest discover -s tests).

Negishi end-to-end

Validated live against real Fortress (see PR comment below once run).

🤖 Generated with Claude Code

Disjoint-union reassembly of a size-routed target from its _vault manifest
(docs/RFC_incremental_v2.md §2.9):
- completeness gate BEFORE any transfer: every selected object tar must
  exist on Fortress (reclaim.hsi_exists) or ABORT — never a partial restore
- empty-dest guard: restore only into a new/empty directory
- per-object extract by recorded transport: htar -xvf -L memberlist
  (random-access indexed) / hsi get + tar xf for htar_large (invariant #6)
- per-file MD5 verify against each object's own run-log source_files
  (arcname-keyed, invariant #2)
- MANDATORY final reconciliation on a full restore:
  set(walk(dest)) == U objects' arcnames, else ABORT
- --slot solo:<hex> / --slot shard:<k> / --member <relpath> selective
  restore (reconciliation is full-restore-only)

LOCAL only (hsi/htar at /opt/hsi/bin, prepended like ship_object). Driven by
either the target's archive config (badge -> manifest) or --manifest directly.

Tests: tests/test_restore.py (17) — tape mocked via reclaim.hsi_exists + an
injected extract fn (mirrors ship_one). Full suite 238 -> 255 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jdoucett
Copy link
Contributor Author

Negishi end-to-end validation — PASSED (2026-07-09, real Fortress, HPSS 10.3.0.p3)

Scratch worktree off ~/fortress-archive at this PR's head (b8b7758); fixture =
4×2 MB small files + one 110 MB file (≥ T_small floor), t_small=100000000,
shard_count=2, durable Depot log_dir (/depot/florasense/etc/logs_fortress/rt_test).

  1. Routed baseline archive: 3 objects shipped (1 solo + 2 shards, K=2, all htar
    with .idx), badge ourjirqp, manifest on Depot _vault/.
  2. FULL restore into empty Depot dest: all 3 objects extracted via
    htar -xvf <tar> -L <memberlist> (random-access, live-confirmed for extract),
    5/5 per-file MD5s verified vs run-logs, final reconciliation OK
    (walk(dest) == manifest aggregate).
  3. Independent check: md5sum -c of the restored tree against source-side
    md5s taken before the archive — 5/5 OK.
  4. --slot shard:0: exactly that shard's 1 member restored; reconciliation
    correctly skipped (selective).
  5. --member sub/small_2.bin: exactly 1 file restored, MD5 == source.
  6. Non-empty dest guard: re-run against the populated dest → [ABORT] ... NOT empty, nothing touched.
  7. Completeness gate LIVE: hsi rm of one shard tar → full restore
    [ABORT] target NOT fully restorable — 1 of 3 object tar(s) missing ... aborting before any extraction; dest never created.

Cleanup done: hsi rm -R rt_restore_e2e (ls rc=64 → gone), Depot src/dest/log
dirs removed, scratch tmp removed, worktree removed.

🤖 Generated with Claude Code

@jdoucett Jarrod S Doucette (jdoucett) deleted the feat/routed-restore branch July 10, 2026 14:23
Sign in to join this conversation on GitHub.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant